/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package bgu.sonar.util.reflections;

import bgu.sonar.util.collections.ImmutableCombinedCollectionView;
import bgu.sonar.util.collections.ImmutableSetView;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 *
 * @author User
 */
public class Registery {

    private Map<Class, Set<Class>> classExtenders = new HashMap<>();
    private Map<Class, Set> registeredInstances = new HashMap<>();

    public void registerAll(Object... instances) {
        for (Object i : instances) {
            register(i);
        }
    }

    public void register(Object instance) {
        if (instance == null) {
            return;
        }

        LinkedList<Class> openList = new LinkedList<>();
        openList.add(instance.getClass());
        while (!openList.isEmpty()) {
            Class c = openList.removeFirst();
            Set set = registeredInstances.get(c);
            if (set == null) {
                for (Class<?> ifc : c.getInterfaces()) {
                    putExtender(ifc, c);
                    openList.add(ifc);
                }

                registeredInstances.put(c, new HashSet());
            }

            if (c != Object.class && !c.isInterface()) {
                putExtender(c.getSuperclass(), c);
                openList.add(c.getSuperclass());
            }
        }


        registeredInstances.get(instance.getClass()).add(instance);

    }

    /**
     * return only instances with the exact class (no extenders)
     *
     * @param <T>
     * @param type
     * @return
     */
    public <T> Collection<T> lookupExact(Class<T> type) {
        Set set = registeredInstances.get(type);
        if (set == null) {
            return Collections.EMPTY_SET;
        }
        return new ImmutableSetView<>(set);
    }

    public <T> Collection<T> lookup(Class<T> type) {
        LinkedList<Class> openList = new LinkedList<>();
        HashSet<Class> closeList = new HashSet<>();

        openList.add(type);
        while (!openList.isEmpty()) {
            Class next = openList.removeFirst();
            if (!closeList.contains(next)) {
                closeList.add(next);
                final Set<Class> extenders = classExtenders.get(next);
                if (extenders != null) {
                    openList.addAll(extenders);
                }
            }
        }

        ArrayList<Collection> all = new ArrayList<>();
        for (Class c : closeList) {
            Set set = registeredInstances.get(c);
            if (set != null && !set.isEmpty()) {
                all.add(set);
            }
        }

        return new ImmutableCombinedCollectionView(all);
    }

    private void putExtender(Class<?> ifc, Class<? extends Object> c) {
        Set<Class> iset = classExtenders.get(ifc);
        if (iset == null) {
            iset = new HashSet<>();
            classExtenders.put(ifc, iset);
        }

        iset.add(c);
    }

    public void inject(Object o) {
        //Field[] fields = o.getClass().getDeclaredFields();
        List<Field> fields = new ArrayList<>();
        for (Class<?> c = o.getClass(); c != null; c = c.getSuperclass()) {
            fields.addAll(Arrays.asList(c.getDeclaredFields()));
        }
        
        for (Field f : fields) {
            f.setAccessible(true);
            Inject injection = f.getAnnotation(Inject.class);
            if (injection != null) {
                Collection all = injection.injectExactType() ? lookupExact(injection.type()) : lookup(injection.type());

                if (f.getType() == Collection.class) {
                    try {
                        f.set(o, all);
                    } catch (IllegalArgumentException | IllegalAccessException ex) {
                        throw new InjectionException("could not modify field value", ex);
                    }
                } else if (all.isEmpty()) {
                    if (injection.exceptionIfCannotFindMatch()) {
                        throw new InjectionException("could not find matching type (" + injection.type().getSimpleName() + " -> " + o.toString() + " {" + o.getClass().getSimpleName() + "/" + f.getName() + "}");
                    }
                } else if (all.size() > 1) {
                    throw new InjectionException("find more then one matching type (" + injection.type().getSimpleName() + " -> " + o.toString() + " {" + o.getClass().getSimpleName() + "/" + f.getName() + "}");
                } else {
                    try {
                        f.set(o, all.iterator().next());
                    } catch (IllegalArgumentException | IllegalAccessException ex) {
                        throw new InjectionException("could not modify field value", ex);
                    }
                }
            }
        }
    }
}
